Skip to main content
Duet’s terminal sharing is built on two key technologies: pseudo-terminals (PTY) for shell I/O and vt10x for terminal state emulation.

Terminal Architecture

The Terminal type in /internal/terminal/terminal.go wraps a PTY with terminal emulation:

Component Breakdown

  • vt: Terminal emulator that parses ANSI/VT100 sequences
  • ptmx: PTY master file handle (connected to shell process)
  • cmd: Shell process (bash, zsh, etc.)
  • width/height: Terminal dimensions in columns/rows
  • workDir: Working directory for shell (room’s workspace)
  • subscribers: Channels for broadcasting updates to clients
  • lastRender: Cached string output (optimization)
  • dirty: Flag indicating render cache needs refresh

Pseudo-Terminal (PTY) Basics

A PTY creates a master-slave pair:
  • Master: Application writes input, reads output
  • Slave: Shell process thinks it’s a real terminal
This allows capturing and sharing shell I/O between multiple clients.

Terminal Initialization

Starting the Terminal

Key Steps:
  1. Create vt10x emulator with terminal size
  2. Determine shell executable ($SHELL or /bin/sh)
  3. Set working directory to room’s workspace
  4. Set TERM=xterm-256color environment variable
  5. Start PTY with creack/pty library
  6. Launch background goroutine to read shell output

Data Flow

Input Flow (Client → Shell)

When a client types:
  1. Bubble Tea converts keystroke to bytes (e.g., "a"[]byte{0x61}, Enter → []byte("\r"))
  2. terminal.Write(data) sends bytes to PTY master
  3. PTY slave (shell) receives input as if from a real terminal
  4. Shell processes command and writes output

Output Flow (Shell → Clients)

Processing Steps:
  1. Read up to 4096 bytes from PTY master
  2. Feed bytes to vt10x emulator (t.vt.Write(buf[:n]))
  3. Mark render cache as dirty
  4. Broadcast update notification to all subscribers

vt10x Terminal Emulator

The vt10x emulator:
  • Parses ANSI/VT100 escape sequences (colors, cursor movement, etc.)
  • Maintains a 2D grid of cells (each with a character and style)
  • Tracks cursor position and visibility
  • Handles terminal modes (insert, wrap, etc.)
This allows converting raw shell output into a renderable terminal state.

Publisher-Subscriber Pattern

Subscription Management

Each client subscribes when joining a room:

Broadcasting Updates

Non-Blocking Design: The select with default ensures slow clients don’t block the readLoop. If a client’s channel buffer is full, the update is skipped (client will get the next one).

Client Update Loop

In the Bubble Tea model:
When terminalUpdateMsg is received:
This creates a loop where the client:
  1. Waits for terminal update notification
  2. Calls terminal.Render() to get latest output
  3. Updates UI model
  4. Starts waiting again

Rendering

Render Method

Rendering Optimizations

1. Caching:
If nothing changed since last render, return cached string. 2. Run-Length Encoding:
Only emit ANSI color codes when colors actually change, reducing output size. 3. Pre-Allocated Buffer:
Pre-allocate buffer to avoid repeated allocations.

Color Conversion

Color Ranges:
  • 0-7: Standard colors (black, red, green, yellow, blue, magenta, cyan, white)
  • 8-15: Bright colors
  • 16-255: Extended 256-color palette

Cursor Rendering

The cursor is rendered by reversing foreground and background colors at the cursor position, creating a visual highlight effect.

Window Resizing

Resize Synchronization:
  1. Update internal dimensions
  2. Invalidate render cache
  3. Resize vt10x emulator grid
  4. Send SIGWINCH to shell via pty.Setsize()
This ensures programs running in the shell (vim, less, etc.) detect the new terminal size.

Client Window Size Handling

When client terminal resizes:
The terminal is resized to fit the client’s window, accounting for sidebars and UI chrome.

Terminal Cleanup

Cleanup Sequence:
  1. Mark terminal as closed
  2. Close all subscriber channels (notifies clients)
  3. Close PTY master file descriptor
  4. Kill shell process
This ensures clean shutdown when the last client leaves a room.

Shared Terminal State

All clients in a room share:
  • Same vt10x instance: Single source of truth for terminal state
  • Same PTY: Input from any client goes to the same shell
  • Same render output: All clients see identical terminal content
Client-Specific:
  • Subscription channels: Each client has its own notification channel
  • Render timing: Clients render independently based on their update loop

Performance Characteristics

Memory Usage

  • vt10x grid: width × height × sizeof(Cell) ≈ 80 × 24 × 16 bytes = 30 KB
  • Render cache: width × height × 4 ≈ 80 × 24 × 4 = 7.6 KB (ANSI sequences add overhead)
  • Read buffer: 4096 bytes per terminal

Latency

  • Input latency: Direct write to PTY (< 1ms)
  • Output latency:
    1. PTY read: kernel buffering (< 1ms)
    2. vt10x parse: O(n) in output bytes (< 1ms for typical output)
    3. Broadcast: O(clients), non-blocking
    4. Client render: Cached if no changes (< 1ms)
Total round-trip latency: < 5ms for typical interactions

Scalability

Per room:
  • Terminal overhead: ~40 KB + shell process
  • Per-client overhead: ~16 bytes (channel in subscriber map)
  • Broadcast complexity: O(n) where n = number of clients
With 100 clients in one room, broadcast is still < 1ms.

Keyboard Input Handling

Special key mappings in /internal/ui/model.go:
These mappings convert Bubble Tea key events to the ANSI sequences shells expect.

Error Handling

Shell Exit Detection

When the shell exits, readLoop terminates gracefully.

Write Failures

Writes to a closed terminal are silently ignored (returns 0 bytes written).

Future Enhancements

Potential improvements:
  • Selective rendering: Only send diffs to clients instead of full frames
  • Replay buffer: Store terminal history for late joiners
  • Input queuing: Buffer input during network lag
  • Compression: Compress render output for slow connections